RustでWebApplicationを実装する
Rustで業務アプリケーションを構築中の駆け出しプログラマーです。
今回は練習のためRustで簡単なWebアプリを実装しようと思います。
概要としては、WebからPOST/GETしてMySQLへ書き込みしたり読み出したり
します。
正直なところ細かいところまで調べきれていないため勉強用記事というようりは備忘録になっています。随時新しい知識が身に付けば更新しまする予定ですのでお手柔らかににお願いします。
前提
各種環境は以下の通りです。エディタはVSコードでRustAnalyzerという
ツールを入れています。
MacOS
cargo 1.46.0
rustc 1.46.0
ysql Ver 8.0.21
プロジェクト作成
% cargo new web_app
Created binary (application) `web_app` package
% cd web_app
actix-web導入
actixとは、Rust製のActorフレームワークです。
(Java/Scalaでいうと、Akkaがメジャーですね)
アクター同士が非同期でメッセージをやり取りし、安全に並行処理を行うことができます。
そのactixをベースとしてWeb開発用機能を追加したのが、
軽量・高速なWeb開発フレームワークであるactix-webです。actix-webで開発されたアプリは、実行ファイルにHTTPサーバーを含んでいるため、
参照元:https://dev.classmethod.jp/articles/actix-web/
そのまま使うこともできるし、apacheやnginxの後ろに置くこともできます。
cargo.tomlファイルにactix-webを使用することを宣言します
[dependencies]
actix-web = "3.1.0"
actix-rt = "1.1.1"
次に、actixを利用してウェブページを返えすためのソースを書きたいところですが今回はMVC的な感じのディレクトリ構成にして実装を進めたいため先にディレクトリを整理します。
以下のような構成にしてください。
ちなみに、ディレクトリ構成についてはこちらサイトを参考にさせていただきました。
web_app
├── Cargo.lock
├── Cargo.toml
├── src
│ ├── controllers
│ │ ├── index.rs
│ │ └── mod.rs
│ ├── lib.rs
│ ├── main.rs
│ ├── models
│ └── routes.rs
└── target
Webページを見れるようにする
それでは早速、main.rsをいじります。
ここら辺についてはこちらを参考にさせていただきました。
参考記事ではroutesというファイルは存在しないのですが、今回はMVC的な感じにするためroutesファイルを作成してルーティングを行います。
この時、routesを利用するよ、というuse宣言を記述し忘れないように気をつけましょう。
use actix_web::{App, HttpServer};
use web_app::routes;
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().configure(routes::routes))
.bind("localhost:8000")?
.run()
.await
}
routes.rsは以下の通りです。
注意点は、routeの最後の()後に「;」が必要なのは常に一番最後の行のrouteになります。これを記述し忘れてはまったので備忘録として。
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use crate::controllers::index;
pub fn routes(cfg: &mut web::ServiceConfig) {
cfg.route("/", web::get().to(index::index));
}
次にlib.rsにモジュールを定義します。
これはroutesとcontrollersを外部参照できるようにするためです。
pub mod routes;
pub mod controllers;
次にcontrollersフォルダ内のmod.rsを以下の通り。
pub mod index;
index.rsを以下の通り。
use actix_web::{web, HttpRequest, HttpResponse, Responder};
pub async fn index() -> impl Responder {
HttpResponse::Ok().body("Hello world!")
}
ここでcargo runを実行して、[localhost:8000]に接続すると
WEBページが帰ってくるはずです。
Postmanの導入
DBへの接続へ進む前に、Get/Postメソッドを簡単に送ることができる便利ツールPostmanの導入をします。
https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop?hl=ja&pv
上記のサイトへいき、chromeの拡張機能へ登録してください。
その後アカウントを登録して、アプリを起動します。
chrome以外での使い方は調べていないので、追加で情報が必要な場合は公式サイトをご覧になると良いかと思います。
公式サイト
ためしにcargo runをしてサーバーを立ち上げた後に、postmanから
localhost:8000/へGETメソッドを送信するとしっかりとHello worldが帰ってきます。
DBへの接続
今回はMySQLへ接続してGet/PostしたいのでMySQLにテスト用データベースを用意します。
MySQLの起動
% mysql.server start
MySQLへ接続
私の場合はパスワードつけてますが、おそらくrootユーザーでパスワード設定していない場合は-pがいらない、かもしれないです。
こちらに関しては各人の設定次第ですね。
% mysql -u[ユーザー名] -p
テスト用のDBを作成
DBの名前は任意です。私はrust_web_testにしました。
% create database rust_web_test
ひとまずMySQはこれでOKです。次にRustのORM「Diesel」をインストールしていきます。
Dieselのインストールに関しては公式のチュートリアルを参考にしました。
まずは依存関係を定義します。
[dependencies]
actix-web = "3.1.0"
actix-rt = "1.1.1"
diesel = { version = "1.4.4", features = ["mysql"] }
dotenv = "0.15.0"
次に、web_app配下に.envファイルを作成してdieselをインストールしましょう。.
cargo install diesel_cli
先ほど作成した.envファイルに今回使用するDBへの接続情報を記述します。
DATABASE_URL=mysql://ユーザー名:パスワード@localhost/rust_web_test
次に以下を実行。
これでweb_app配下にmigrationフォルダが作成されここでschemaをマネジメントしていきます。
diesel setup
試しにテーブルを作成してみましょう。
以下を実行するとmigrationフォルダの下に新しいフォルダが作成され、その配下に
・up.sql
・down.sql
が作成されます。
% diesel migration generate users
up.sqlとdown.sqlにSQLを記述
CREATE TABLE users
(
id SERIAL PRIMARY KEY,
name VARCHAR(64) NOT NULL
);
DROP TABLE users;
ターミナルで以下を実行して変更を反映させます。これでSQLは以上です。
% diesel migration run
Running migration 2020-10-14-003024_users
% diesel migration redo
Rolling back migration 2020-10-14-003024_users
Running migration 2020-10-14-003024_users
これでshema.rsには以下のようなソースが追加されます。
table! {
users (id) {
id -> Unsigned<Bigint>,
name -> Varchar,
}
}
試しに、MySQLに接続してテーブルを確認すると「users」テーブルが作成されているはずです。
//接続
% mysql -uユーザー名 -p
//use
mysql> use rust_web_app
//show
mysql> show tables;
+----------------------------+
| Tables_in_rust_web_app |
+----------------------------+
| __diesel_schema_migrations |
| users |
+----------------------------+
2 rows in set (0.00 sec)
ここまでやると自動的に以下のようなディレクトリになっていると思います。
util.rsは自分で付け足しました。
ここにはMySQLへ接続するための関数を記述します。
web_app
├── Cargo.lock
├── Cargo.toml
├── diesel.toml
├── migrations
│ └── 2020-10-14-003024_users
│ ├── down.sql
│ └── up.sql
├── src
│ ├── controllers
│ │ ├── index.rs
│ │ └── mod.rs
│ ├── lib.rs
│ ├── main.rs
│ ├── models
│ ├── routes.rs
│ ├── schema.rs
└── target
ついでにlib.rsを更新しておきましょう。
さらにファイルを追加していきます。
・util.rs
・models/mod.rs
・models/users.rs
・controllers/create_user
以下のようなディレクトリになってればOKです。
web_app
├── Cargo.lock
├── Cargo.toml
├── diesel.toml
├── migrations
│ └── 2020-10-14-062158_users
│ ├── down.sql
│ └── up.sql
├── src
│ ├── controllers
│ │ ├── create_user.rs
│ │ ├── index.rs
│ │ └── mod.rs
│ ├── lib.rs
│ ├── main.rs
│ ├── models
│ │ ├── mod.rs
│ │ └── users.rs
│ ├── routes.rs
│ └── schema.rs
│ └── util.rs
├── target
├── .env
まずは各ファイルを外部参照できるように、lib.rsを以下のようにします。
#[macro_use]
extern crate diesel;
extern crate dotenv;
pub mod routes;
pub mod controllers;
pub mod models;
pub mod schema;
pub mod util;
DBへ値を書き込む
controllers配下のmod.rsに以下を追記します。
pub mod index;
pub mod create_user;
次にmodels配下のmod.rsに以下を追記
pub mod users;
create_user.rsに戻って以下を追記してください。
今回は「name」だけをDBに書き込みます。
extern crate diesel;
use crate::models::users::User;
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct UserData {
name: String,
}
pub async fn create(item: web::Json<UserData>) -> HttpResponse {
let user = User::create(&(item.name));
println!("{:?}",user);
HttpResponse::Created().body("Inserting")
}
models/users.rsを以下の通り
use diesel::prelude::*;
use crate::schema::users;
use crate::util::establish_connection;
#[derive(Debug, Queryable)]
pub struct User {
pub id: u64,
pub name: String,
}
#[derive(Insertable)]
#[table_name = "users"]
pub struct NewUser<'a> {
name: &'a str,
}
impl User {
pub fn all() -> Vec<User> {
let connection = establish_connection();
users::dsl::users
.limit(30)
.load::<User>(&connection)
.expect("Error loading users")
}
pub fn create(name: &str) -> User {
use self::users::id;
let new_user = NewUser { name: name };
let connection = establish_connection();
diesel::insert_into(users::table)
.values(&new_user)
.execute(&connection)
.expect("Error saving new user");
users::dsl::users
.order(id.desc())
.first::<User>(&connection)
.expect("Error finding users")
}
}
次に、MySQLとのコネクションをするための関数をutilに記述します。
use diesel::mysql::MysqlConnection;
use diesel::prelude::*;
use dotenv::dotenv;
use std::env;
pub fn establish_connection() -> MysqlConnection {
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
MysqlConnection::establish(&database_url)
.expect(&format!("Error connecting to {}", database_url))
}
お次はcreate_userでシリアライズというアトリビュートを使用するので依存関係を定義します。
[dependencies]
actix-web = "3.1.0"
actix-rt = "1.1.1"
diesel = { version = "1.4.4", features = ["mysql"] }
dotenv = "0.15.0"
serde = { version = "1.0.116", features = ["derive"] }
serde_json = "1.0.58"
最後にroutes.rsにcreate_userを追記して書き込みができるようになります。
extern crate diesel;
use crate::models::users::User;
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct UserData {
name: String,
}
pub async fn create(item: web::Json<UserData>) -> HttpResponse {
let user = User::create(&(item.name));
println!("{:?}",user);
HttpResponse::Created().body("Inserting")
}
cargo runを実行してpostmanでメソッドをGETからPOSTへ変更し、
アドレスをlocalhost:8000/create_userにしてください。
アドレスのすぐ下にあるタブに、「Headers」があるので
そこのKeyに「Content-Type」Valueに「application/json」と設定しましょう。
次にBodyタブをクリックしてJson形式で値を入力してSendすれば値を書き込めます。
ターミナルログ
Finished dev [unoptimized + debuginfo] target(s) in 30.12s
Running `target/debug/web_app`
User { id: 2, name: "testuser1" }
MySQLに接続してテーブルの中身を確認すれば値が格納されているはずです。
DBの読み込み
次は格納した値をGetメソッドで呼び出してみたいと思います。
ルートにアクセスした際に格納した値を取り出す処理にしようと思いますので、
indexファイルを以下のように修正します。
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use crate::models::users::User;
pub async fn index() -> impl Responder {
let results = User::all();
let mut res = format!("Displaying {} users\n\n", results.len());
for user in results {
let s = format!("id: {}, name: {}\n", user.id, user.name);
res.push_str(&s);
}
HttpResponse::Ok().body(res)
}
終わりも近づいてきました。
PostmanでGetメソッドでルートにアクセスしてSendすると格納した値を取得できます。私は事前にtestuserを格納しているので値が2つありますね。
これで今回の目的は達成しました。
ほとんど備忘録的な感じで解説をはさめていないため、随時更新して解説も入れていきたいと思います。(自分の勉強のため)
この記事へのコメントはありません。